{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 遮盖和填充"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "\n",
    "import tensorflow as tf\n",
    "\n",
    "from tensorflow.keras import layers"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 填充序列数据\n",
    "处理序列数据时，每个样本具有不同的长度是很常见的。考虑以下示例（标记为单词的文本）：\n",
    "```txt\n",
    "[\n",
    "  [\"The\", \"weather\", \"will\", \"be\", \"nice\", \"tomorrow\"],\n",
    "  [\"How\", \"are\", \"you\", \"doing\", \"today\"],\n",
    "  [\"Hello\", \"world\", \"!\"]\n",
    "]\n",
    "```\n",
    "在词汇查询之后，数据可能会被矢量化为整数，例如：\n",
    "```txt\n",
    "[\n",
    "  [83, 91, 1, 645, 1253, 927],\n",
    "  [73, 8, 3215, 55, 927],\n",
    "  [71, 1331, 4231]\n",
    "]\n",
    "```\n",
    "数据是 2D 列表，其中各个样本的长度分别为6、5和3。由于深度学习模型的输入数据必须是单个张量（在这种情况下，其形状为例如（batch_size，6，vocab_size）的形状），因此，比最长的项目短的样本需要填充一些占位符值（或者，一个占位符）。可能还会在填充短样本之前截断长样本）。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[[  83   91    1  645 1253  927]\n",
      " [  73    8 3215   55  927    0]\n",
      " [ 711  632   71    0    0    0]]\n"
     ]
    }
   ],
   "source": [
    "raw_inputs = [\n",
    "  [83, 91, 1, 645, 1253, 927],\n",
    "  [73, 8, 3215, 55, 927],\n",
    "  [711, 632, 71]\n",
    "]\n",
    "\n",
    "# 默认情况下，填充数据将使用零;它可以通过 \"value\" 参数进行配置。\n",
    "# 注意，你可以“前”填充(在开头)或“后”填充(在结尾)。\n",
    "# 建议在处理 RNN 层时使用  \"post\" 填充(以便能够使用层的 CuDNN  实现)。\n",
    "padded_inputs = tf.keras.preprocessing.sequence.pad_sequences(raw_inputs,\n",
    "                                                              padding='post')\n",
    "\n",
    "print(padded_inputs)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 遮盖\n",
    "现在所有的样例都有了统一的长度，必须告知模型，数据的某些部分实际上是填充的，应该忽略。该机制就是遮盖。\n",
    "在 Keras 模型中有三种方法用于遮盖：\n",
    "1. 添加一个 keras.layers.Masking 层\n",
    "2. 定义一个 mask_zeros=True 的嵌入层\n",
    "3. 在调用支持  mask 参数的图层（例如RNN图层）时，手动传递一个 mask 参数。\n",
    "\n",
    "## 遮罩生成层：嵌入和遮盖\n",
    "在底层，这些层将创建一个遮罩张量（形状为（batch，sequence_length）的 2D 张量），并将其附加到由 Masking 或 Embedding 层返回的张量输出。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "tf.Tensor(\n",
      "[[ True  True  True  True  True  True]\n",
      " [ True  True  True  True  True False]\n",
      " [ True  True  True False False False]], shape=(3, 6), dtype=bool)\n",
      "tf.Tensor(\n",
      "[[ True  True  True  True  True  True]\n",
      " [ True  True  True  True  True False]\n",
      " [ True  True  True False False False]], shape=(3, 6), dtype=bool)\n"
     ]
    }
   ],
   "source": [
    "embedding = layers.Embedding(input_dim=5000, output_dim=16, mask_zero=True)\n",
    "masked_output = embedding(padded_inputs)\n",
    "\n",
    "print(masked_output._keras_mask)\n",
    "\n",
    "masking_layer = layers.Masking()\n",
    "# 通过将 2D 输入扩展为 3D (嵌入维数为 10)来模拟嵌入查找。\n",
    "unmasked_embedding = tf.cast(\n",
    "    tf.tile(tf.expand_dims(padded_inputs, axis=-1), [1, 1, 10]),\n",
    "    tf.float32)\n",
    "\n",
    "masked_embedding = masking_layer(unmasked_embedding)\n",
    "print(masked_embedding._keras_mask)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "从打印结果中可以看到，掩码是具有形状（batch_size，sequence_length）的 2D boolean张量，其中每个 False 条目表示在处理期间应忽略相应的时间步长。\n",
    "\n",
    "## 函数式 API 和顺序 API 中的掩码传播\n",
    "使用函数式 API 或顺序 API 时，对于可以使用它们的任何层（例如，RNN层），由嵌入或掩蔽层生成的掩码将通过网络传播。 Keras 将自动获取与输入相对应的遮罩，并将其传递给知道如何使用该遮罩的任何层。\n",
    "\n",
    "注意：在子类化模型或图层的调用方法中，不会自动传播遮罩，因此你需要手动将遮罩参数传递给需要一个图层的任何图层。\n",
    "\n",
    "有关详细信息，参见以下部分。 例如，在以下顺序模型中，LSTM 层将自动接收掩码，这意味着它将忽略填充值："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "model = tf.keras.Sequential([\n",
    "  layers.Embedding(input_dim=5000, output_dim=16, mask_zero=True),\n",
    "  layers.LSTM(32),\n",
    "])\n",
    "\n",
    "# 以下函数式 API 模型也是如此：\n",
    "inputs = tf.keras.Input(shape=(None,), dtype='int32')\n",
    "x = layers.Embedding(input_dim=5000, output_dim=16, mask_zero=True)(inputs)\n",
    "outputs = layers.LSTM(32)(x)\n",
    "\n",
    "model = tf.keras.Model(inputs, outputs)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 将遮罩张量直接传递到图层\n",
    "可以处理掩码的层（例如 LSTM 层）在其 __call__ 方法中具有 mask 参数。 \n",
    "\n",
    "同时，产生遮罩（例如嵌入）的图层会公开你可以调用的 compute_mask(input，previous_mask) 方法。 \n",
    "\n",
    "因此，你可以执行以下操作："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class MyLayer(layers.Layer):\n",
    "  \n",
    "  def __init__(self, **kwargs):\n",
    "    super(MyLayer, self).__init__(**kwargs)\n",
    "    self.embedding = layers.Embedding(input_dim=5000, output_dim=16, mask_zero=True)\n",
    "    self.lstm = layers.LSTM(32)\n",
    "    \n",
    "  def call(self, inputs):\n",
    "    x = self.embedding(inputs)\n",
    "    # 注意，你也可以手动准备一个 \"蒙版\" 张量。它只需要是一个具有正确形状的布尔张量，\n",
    "    # 例如(batch_size, timesteps)。\n",
    "    mask = self.embedding.compute_mask(inputs)\n",
    "    output = self.lstm(x, mask=mask)  # 该层将忽略屏蔽的值\n",
    "    return output\n",
    "\n",
    "layer = MyLayer()\n",
    "x = np.random.random((32, 10)) * 100\n",
    "x = x.astype('int32')\n",
    "layer(x)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 在你的自定义图层中支持遮罩\n",
    "有时，你可能需要编写生成遮罩的图层（如 \"嵌入\" ），或编写需要修改当前遮罩的图层。 \n",
    "\n",
    "比如，任何产生张量的时间维度与其输入不同的层，例如连接时间维度的连接层，都需要修改当前掩码，以便下游的层能够适当地考虑掩码的时间步长。\n",
    "\n",
    "为此，你的图层应实现 layer.compute_mask() 方法，该方法将在给定输入和当前掩码的情况下生成一个新的掩码。 \n",
    "\n",
    "大多数图层都不会修改时间维度，因此无需担心掩盖。在这种情况下，compute_mask（）的默认行为只是传递当前掩码。 \n",
    "\n",
    "这是需要修改当前蒙版的 TensorFlowSplit 图层的示例。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class TemporalSplit(tf.keras.layers.Layer):\n",
    "  \"\"\"将输入张量沿时间维分裂成2个张量\"\"\"\n",
    "\n",
    "  def call(self, inputs):\n",
    "    # 期望输入为3D，掩模为2D，将输入张量沿时间轴(轴1)分成2个子张量。\n",
    "    return tf.split(inputs, 2, axis=1)\n",
    "    \n",
    "  def compute_mask(self, inputs, mask=None):\n",
    "    # 如果它出现，也把遮盖分成2份。\n",
    "    if mask is None:\n",
    "      return None\n",
    "    return tf.split(mask, 2, axis=1)\n",
    "\n",
    "first_half, second_half = TemporalSplit()(masked_embedding)\n",
    "print(first_half._keras_mask)\n",
    "print(second_half._keras_mask)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这是 CustomEmbedding 层的另一个示例，该层能够根据输入值生成掩码："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class CustomEmbedding(tf.keras.layers.Layer):\n",
    "  \n",
    "  def __init__(self, input_dim, output_dim, mask_zero=False, **kwargs):\n",
    "    super(CustomEmbedding, self).__init__(**kwargs)\n",
    "    self.input_dim = input_dim\n",
    "    self.output_dim = output_dim\n",
    "    self.mask_zero = mask_zero\n",
    "    \n",
    "  def build(self, input_shape):\n",
    "    self.embeddings = self.add_weight(\n",
    "      shape=(self.input_dim, self.output_dim),\n",
    "      initializer='random_normal',\n",
    "      dtype='float32')\n",
    "    \n",
    "  def call(self, inputs):\n",
    "    return tf.nn.embedding_lookup(self.embeddings, inputs)\n",
    "  \n",
    "  def compute_mask(self, inputs, mask=None):\n",
    "    if not self.mask_zero:\n",
    "      return None\n",
    "    return tf.not_equal(inputs, 0)\n",
    "  \n",
    "  \n",
    "layer = CustomEmbedding(10, 32, mask_zero=True)\n",
    "x = np.random.random((3, 10)) * 9\n",
    "x = x.astype('int32')\n",
    "\n",
    "y = layer(x)\n",
    "mask = layer.compute_mask(x)\n",
    "\n",
    "print(mask)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 编写需要遮罩信息的图层\n",
    "某些层是 mask 使用者：它们在调用中接受 mask 参数，并使用它确定是否跳过某些时间步长。 \n",
    "\n",
    "要编写这样的层，你只需在调用中添加 mask=None即可。只要有输入，与输入关联的掩码将被传递到你的图层。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "class MaskConsumer(tf.keras.layers.Layer):\n",
    "  \n",
    "  def call(self, inputs, mask=None):\n",
    "    pass"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 概括\n",
    "这就是关于Keras中的蒙版所需要了解的所有内容。回顾一下：\n",
    "- \"遮盖\" 是图层如何知道何时跳过/忽略序列输入中的某些时间步长的方法。\n",
    "- 一些层是遮罩生成器：嵌入可以根据输入值（如果 mask_zero = True）生成遮罩，遮罩层也可以。\n",
    "- 有些层是 mask-consumers（遮盖消费者）：它们在 __call__ 方法中公开 mask 参数。 RNN 层就是这种情况。\n",
    "- 在函数式 API和顺序API中，掩码信息会自动传播。\n",
    "- 在编写子类化模型或以独立方式使用图层时，要手动将 mask 参数传递给图层。\n",
    "- 你可以轻松地编写修改当前蒙版，生成新蒙版或使用与输入关联的蒙版的图层。"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.6rc1"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
